﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Reflection;

namespace Carbon.Web.Forms
{
    public class Binder
    {
        private String m_DataPath = default(String);
        public String DataPath
        {
            get
            {
                return m_DataPath;
            }
            set
            {
                m_DataPath = value;
            }
        }

        private Object m_Root = default(Object);
        public Object Root
        {
            get
            {
                return m_Root;
            }
        }

        private bool m_TargetObjectMustExist = default(bool);
        public bool TargetObjectMustExist
        {
            get
            {
                return m_TargetObjectMustExist;
            }
        }

        public Binder(object root, string dataPath, bool targetObjectMustExist)
            : base()
        {
            if (dataPath == null) throw new ArgumentNullException("dataPath");
            if (root == null) throw new ArgumentNullException("root");

            this.m_DataPath = dataPath;
            this.m_Root = root;
            this.m_TargetObjectMustExist = targetObjectMustExist;
        }

        #region Static Fields (2)
        private static readonly BindingFlags ReflectionBindingFlags = BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic;
        private static readonly String[] PathSeparator = new String[] { "." };
        private static readonly String ThisKeyword = "this";
        #endregion

        private PropertyInfo m_Property;

        #region Properties (13)
        public PropertyInfo Property
        {
            get
            {
                CalculateTarget();

                if (m_Property == null)
                    throw new InvalidOperationException("Not targeting property.");

                return m_Property;
            }
        }

        public bool TargetIsProperty
        {
            get
            {
                CalculateTarget();

                return m_Property != null;
            }
        }

        public bool IsNullable
        {
            get
            {
                return InternalUtility.IsNullable(Property.PropertyType);
            }
        }

        public bool IsReadOnly
        {
            get
            {
                CalculateTarget();

                bool isReadOnly = false;
                if (m_Property == null)
                    isReadOnly = true;
                else
                    isReadOnly = !Property.CanWrite;

                return isReadOnly;
            }
        }

        private void WrapException(Action action)
        {
            try
            {
                action();
            }
            catch (BindingException)
            {
                throw;
            }
            catch (Exception ex)
            {
                if (ex is TargetInvocationException)
                    ex = ex.InnerException;

                throw new BindingException(this.DataPath, this.Root, ex);
            }
        }

        private T WrapException<T>(Func<T> action)
        {
            try
            {
                return action();
            }
            catch (BindingException)
            {
                throw;
            }
            catch (Exception ex)
            {
                if (ex is TargetInvocationException)
                    ex = ex.InnerException;

                throw new BindingException(this.DataPath, this.Root, ex);
            }
        }

        private String[] m_PathSections = default(String[]);
        private bool m_PathSections_calculated = false;
        public String[] PathSections
        {
            get
            {
                if (!m_PathSections_calculated)
                {
                    if (string.IsNullOrEmpty(DataPath)) throw new ArgumentOutOfRangeException("DataPath");
                    m_PathSections = DataPath.Split(PathSeparator, StringSplitOptions.RemoveEmptyEntries);

                    foreach (String pathSection in m_PathSections)
                    {
                        if (string.IsNullOrEmpty(pathSection))
                            throw new ArgumentOutOfRangeException("Path section is null.");
                    }

                    m_PathSections_calculated = true;
                }

                return m_PathSections;
            }
        }
        private Object m_TargetObject = default(Object);
        public Object TargetObject
        {
            get
            {
                CalculateTarget();

                return m_TargetObject;
            }
        }

        public Object Value
        {
            get
            {
                if (TargetObjectMustExist)
                {
                    if (TargetObject == null)
                        throw new NullReferenceException("TargetObject");
                }

                if (DataPath == ThisKeyword)
                    return TargetObject;

                if (TargetObject != null)
                    return Property.GetValue(TargetObject, null);
                else
                    return null;
            }
            set
            {
                if (!IsNullable && value == null)
                    throw new InvalidOperationException("Setting 'null' value to non nullable property.");

                if ((Property != null) == false) throw new InvalidOperationException("(Property != null) == false");

                Property.SetValue(TargetObject, value, null);
            }
        }
        #endregion

        #region Methods (3)

        private bool m_CalculatedTarget = default(bool);

        private void CalculateTarget()
        {
            if (!m_CalculatedTarget)
            {
                if (Root == null)
                {
                    if (TargetObjectMustExist)
                        throw new InvalidOperationException("Target object must exist.");
                    else
                    {
                        m_CalculatedTarget = true;

                        m_Property = null;
                        m_TargetObject = null;

                        return;
                    }
                }

                if ((PathSections.Length > 0) == false) throw new InvalidOperationException("(PathSections.Length > 0) == false");

                Object targetObject = Root;
                PropertyInfo propertyInfo = null;

                String[] pathSections = PathSections;
                for (int i = 0; i < pathSections.Length - 1; ++i)
                {
                    String pathSection = PathSections[i];

                    if (pathSection == ThisKeyword)
                    {
                        continue;
                    }
                    else
                    {
                        propertyInfo = targetObject.GetType().GetProperty(pathSection, ReflectionBindingFlags);

                        if (propertyInfo == null)
                            throw new InvalidBindingPath(DataPath, targetObject, null);

                        targetObject = propertyInfo.GetValue(targetObject, null);
                        if (targetObject == null)
                            throw new InvalidBindingPath(this.DataPath, Root, null);
                    }
                }

                if (pathSections[pathSections.Length - 1] != ThisKeyword)
                {
                    propertyInfo = targetObject.GetType().GetProperty(pathSections[pathSections.Length - 1], ReflectionBindingFlags);

                    if (propertyInfo == null)
                        throw new InvalidBindingPath(DataPath, targetObject, null);
                }
                else
                {
                    propertyInfo = null;
                }

                m_Property = propertyInfo;
                m_TargetObject = targetObject;

                m_CalculatedTarget = true;
            }
        }

        #endregion
    }
}
